package com.ihr360.job.core.listener;



import com.ihr360.job.core.job.builder.JobBuilderHelper;
import com.ihr360.job.core.support.MethodInvoker;
import com.ihr360.job.core.support.MethodInvokerUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.util.Assert;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * {@link FactoryBean} implementation that builds a listener based on the
 * various lifecycle methods or annotations that are provided. There are three
 * possible ways of having a method called as part of a listener lifecycle:
 *
 * <ul>
 * <li>Interface implementation: By implementing any of the subclasses of a
 * listener interface, methods on said interface will be called
 * <li>Annotations: Annotating a method will result in registration.
 * <li>String name of the method to be called, which is tied to a
 * {@link ListenerMetaData} value in the metaDataMap.
 * </ul>
 *
 * It should be noted that methods obtained by name or annotation that don't
 * match the listener method signatures to which they belong will cause errors.
 * However, it is acceptable to have no parameters at all. If the same method is
 * marked in more than one way. (i.e. the method name is given and it is
 * annotated) the method will only be called once. However, if the same class
 * has multiple methods tied to a particular listener, each method will be
 * called. Also note that the same annotations cannot be applied to two separate
 * methods in a single class.
 *
 * @author Lucas Ward
 * @author Dan Garrette
 * @since 2.0
 * @see ListenerMetaData
 */
public abstract class AbstractListenerFactoryBean<T> implements FactoryBean<Object>, InitializingBean {
    protected static final Logger logger = LoggerFactory.getLogger(JobBuilderHelper.class.getName());
    private Object delegate;

    private Map<String, String> metaDataMap;

    @Override
    public Object getObject() {
        if (metaDataMap == null) {
            metaDataMap = new HashMap<String, String>();
        }
        // Because all annotations and interfaces should be checked for, make
        // sure that each meta data
        // entry is represented.
        for (ListenerMetaData metaData : this.getMetaDataValues()) {
            if (!metaDataMap.containsKey(metaData.getPropertyName())) {
                // put null so that the annotation and interface is checked
                metaDataMap.put(metaData.getPropertyName(), null);
            }
        }

        Set<Class<?>> listenerInterfaces = new HashSet<Class<?>>();

        // For every entry in the map, try and find a method by interface, name,
        // or annotation. If the same
        Map<String, Set<MethodInvoker>> invokerMap = new HashMap<String, Set<MethodInvoker>>();
        boolean synthetic = false;
        for (Map.Entry<String, String> entry : metaDataMap.entrySet()) {
            final ListenerMetaData metaData = this.getMetaDataFromPropertyName(entry.getKey());
            Set<MethodInvoker> invokers = new HashSet<MethodInvoker>();

            MethodInvoker invoker;
            invoker = MethodInvokerUtils.getMethodInvokerForInterface(metaData.getListenerInterface(), metaData.getMethodName(), delegate,
                    metaData.getParamTypes());
            if (invoker != null) {
                invokers.add(invoker);
            }

            invoker = getMethodInvokerByName(entry.getValue(), delegate, metaData.getParamTypes());
            if (invoker != null) {
                invokers.add(invoker);
                synthetic = true;
            }

            if(metaData.getAnnotation() != null) {
                invoker = MethodInvokerUtils.getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate, metaData.getParamTypes());
                if (invoker != null) {
                    invokers.add(invoker);
                    synthetic = true;
                }
            }

            if (!invokers.isEmpty()) {
                invokerMap.put(metaData.getMethodName(), invokers);
                listenerInterfaces.add(metaData.getListenerInterface());
            }

        }

        if (listenerInterfaces.isEmpty()) {
            listenerInterfaces.add(this.getDefaultListenerClass());
        }

        if (!synthetic) {
            int count = 0;
            for (Class<?> listenerInterface : listenerInterfaces) {
                if (listenerInterface.isInstance(delegate)) {
                    count++;
                }
            }
            // All listeners can be supplied by the delegate itself
            if (count == listenerInterfaces.size()) {
                return delegate;
            }
        }

        boolean ordered = false;
        if (delegate instanceof Ordered) {
            ordered = true;
            listenerInterfaces.add(Ordered.class);
        }

        // create a proxy listener for only the interfaces that have methods to
        // be called
        ProxyFactory proxyFactory = new ProxyFactory();
        if (delegate instanceof Advised) {
            proxyFactory.setTargetSource(((Advised) delegate).getTargetSource());
        }
        else {
            proxyFactory.setTarget(delegate);
        }
        proxyFactory.setInterfaces(listenerInterfaces.toArray(new Class[0]));
        proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new MethodInvokerMethodInterceptor(invokerMap, ordered)));
        return proxyFactory.getProxy();

    }

    protected abstract ListenerMetaData getMetaDataFromPropertyName(String propertyName);

    protected abstract ListenerMetaData[] getMetaDataValues();

    protected abstract Class<?> getDefaultListenerClass();

    protected MethodInvoker getMethodInvokerByName(String methodName, Object candidate, Class<?>... params) {
        if (methodName != null) {
            return MethodInvokerUtils.getMethodInvokerByName(candidate, methodName, false, params);
        }
        else {
            return null;
        }
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    public void setDelegate(Object delegate) {
        this.delegate = delegate;
    }

    public void setMetaDataMap(Map<String, String> metaDataMap) {
        this.metaDataMap = metaDataMap;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(delegate, "Delegate must not be null");
    }

    /**
     * Convenience method to check whether the given object is or can be made
     * into a listener.
     *
     * @param target the object to check
     * @return true if the delegate is an instance of any of the listener
     * interface, or contains the marker annotations
     */
    public static boolean isListener(Object target, Class<?> listenerType, ListenerMetaData[] metaDataValues) {
        if (target == null) {
            return false;
        }
        if (listenerType.isInstance(target)) {
            return true;
        }
        if (target instanceof Advised) {
            TargetSource targetSource = ((Advised) target).getTargetSource();
            if (targetSource != null && targetSource.getTargetClass() != null
                    && listenerType.isAssignableFrom(targetSource.getTargetClass())) {
                return true;
            }

            if(targetSource != null && targetSource.getTargetClass() != null && targetSource.getTargetClass().isInterface()) {
                logger.warn(String.format("%s is an interface.  The implementing class will not be queried for annotation based listener configurations.  If using @StepScope on a @Bean method, be sure to return the implementing class so listener annotations can be used.", targetSource.getTargetClass().getName()));
            }
        }
        for (ListenerMetaData metaData : metaDataValues) {
            if (MethodInvokerUtils.getMethodInvokerByAnnotation(metaData.getAnnotation(), target) != null) {
                return true;
            }
        }
        return false;
    }
}